/*
 * @Author: D.Y.X
 * @Date: 2024-08-13 23:00:35
 * @LastEditors: D.Y.X
 * @LastEditTime: 2024-10-14 10:35:21
 * @FilePath: \flex-\someMathStore.js
 * @Description: 这是一个数学类的函数仓库
*/

// 数集、数组、一个向量（默认行向量）的格式
/* 
    let arr = [1, -2, 0, 24, -9, ...];
    // 需要注意的是，行向量的表达式：
    let vector = [1, 2, 6, -3, ...]  <====>  let vector = [[1, 2, 6, -3, ...]]
        这二者等价。
    // 特殊的，列向量表达式：
    let vector = [
        [1],
        [5],
        [-2],
        [0],
        ...
    ]
*/
// 小数，假分数形式
/* 
    let ferNum = 0.91
    let ferNum = "15 / 4"   // 四分之十五
    let ferNum = "-b / a"   // 负 a 分之 b
*/
// 点的格式
/* 
    let point = {
        x: 10,  // 或地理中代表经度
        y: 15,  // 或地理中代表纬度
    };
*/
// 多个点格式（点集），再多个点以此类推。
/* 
    let pointArr = [
        {
            x: 10,
            y: 15,
        }, {
            x: 20,
            y: 1
        },
        ...
    ];
*/
// 直线格式
/* 
    // 直线通用表达式： Ax + By + C = 0
    let line = {
        A,
        B,
        C,
    };
    // 有时用 2 个点表示直线
    let line = [
        {
            x: 10,
            y: 15,
        }, {
            x: 20,
            y: 1
        }
    ]
*/
// 二维矩阵、向量集的格式
/* 
    // 例如一个 2 * 3 的矩阵
    let matrix = [
        [1, 2, 4],
        [0, 9, -2]
    ]
    let array = [
        1, 3, 8, 9, 0, ...
    ]
*/



/**
 * @description: 判断数集中是否全部同号或者有 0
 * @param {*} arr: 数集
 * @return { boolean } true 表示数集中除了 0 之外都同号或者全 0；false 表示数集中存在异号
 */
function isSameSignal(arr) {
    if (!arr) {
        throw new Error("请输入合法数集参数！")
    }
    if (arr.length === 0) {
        throw new Error("请输入的数集参数中至少有一项！")
    }
    if (arr.length === 1) {
        // 数集只有 1 项
        return true;
    }
    if (Math.max(...arr) === 0 && Math.min(...arr) === 0) {
        // 全 0
        return true;
    }
    let flag = true;    // 标识
    if (Math.max(...arr) * Math.min(...arr) < 0) {
        flag = false;
    } else if (Math.max(...arr) * Math.min(...arr) > 0) {
        flag = true;
    } else {
        // 数集中带 0 ，这种情况比较复杂。
        if (Math.min(...arr) === 0 || Math.max(...arr) === 0) {
            // 数集虽然出现 0，但是除了 0 之外都同号
            flag = true;
        } else {
            // 数组虽然出现 0，但是仍然是不同号
            flag = false;
        }
    }

    return flag;
}

/**
 * @description: 求两点之间的距离
 * @param {*} point1
 * @param {*} point2
 * @return { number } 距离
 */
function getDistanceBeforeTwoPoints(point1, point2) {
    return Math.sqrt(
        Math.pow(point2.y - point1.y, 2) + Math.pow(point2.x - point1.x, 2)
    );
}

/**
 * @description: 计算根据基准点(basePoint)为原点的基础坐标系，返回的是以该点为原点下的其他点的对应坐标
 * @param {*} basePoint
 * @param {*} otherPoint
 * @param { boolean } isHaveSelf 表示 basePoint 如果包含于 otherPoint 中，是否需要剔除出去这个点。
 * @return [{ x, y }, { x, y } ] 点坐标
 */
function changePointToBase(basePoint, otherPoint, isHaveSelf = false) {
    if (isHaveSelf) {
        otherPoint = otherPoint.filter(
            (point) => point.x != basePoint.x && point.y != basePoint.y
        );
    }
    return otherPoint.map(point => {
        return {
            ...point, // 保留其他信息
            x: point.x - basePoint.x,
            y: point.y - basePoint.y
        }
    })
}

/**
 * @description: 筛选重复点，保留非重复点
 * @param {*} pointArr
 * @param {*} pointArr
 * @return [{ x, y }, { x, y } ] 点坐标
 */
function deleteRepeatPoint(pointArr) {
    if (!pointArr) {
        throw new Error("请输入合法点集参数！")
    }
    if (pointArr.length === 1) {
        throw new Error("输入的点至少要有 2 个！")
    }
    let repeatIndex = [];
    for (let i = 0; i < pointArr.length - 1; i++) {
        for (let j = i + 1; j < pointArr.length; j++) {
            if (
                cross2Product(pointArr[i], pointArr[j]) === 0 &&
                getDistanceBeforeTwoPoints(pointArr[i], { x: 0, y: 0 }) ===
                getDistanceBeforeTwoPoints(pointArr[j], { x: 0, y: 0 })
            ) {
                repeatIndex.push(j);
            }
        }
    }
    [...new Set(repeatIndex)].forEach((it) => {
        pointArr[it].__DELETE__DYX___ = true;
    });
    return pointArr.filter((it) => !it.__DELETE__DYX___);
}

/**
 * @description: 输入点集，获取点集的三点共线组合的序号
 * @param {*} pointArr
 * @param { boolean } needDeletRepeatPoint 是否提前删除重复点
 * @return { Array } 三点共线的序号组合
 */
function pointCollinearGroup(pointArr, needDeletRepeatPoint = true) {
    let newPoint = pointArr;
    // 去除重复点
    if (needDeletRepeatPoint) {
        newPoint = deleteRepeatPoint(pointArr);
    }
    let repeatPoint = [];

    for (let i = 0; i < newPoint.length; i++) {
        for (let j = 0; j < newPoint.length; j++) {
            if (i === j || j < i) {
                continue;
            }
            for (let ii = 0; ii < newPoint.length; ii++) {
                for (let jj = 0; jj < newPoint.length; jj++) {
                    if (ii === jj || jj < ii) {
                        continue;
                    }
                    if (i === ii && j === jj) {
                        continue;
                    }
                    if (j > jj || (j === jj && i > ii)) {
                        continue;
                    }

                    if (
                        cross2Product(
                            getPointArray(newPoint[i], newPoint[j]),
                            getPointArray(newPoint[ii], newPoint[jj])
                        ) === 0
                    ) {
                        let yarr = [...new Set([i, j, ii, jj])].sort((a, b) => a - b);
                        repeatPoint.push(yarr);
                        repeatPoint = repeatPoint.filter((it) => it.length === 3);
                    }
                }
            }
        }
    }
    repeatPoint = [...new Set(repeatPoint.map((it) => it.join()))];
    return repeatPoint;
}

/**
 * @description: 标记已知共线点的中间点，保留直线上的两侧点
 * @param {*} pointArr  // 输入的点已经共线
 * @return [{ x, y }, { x, y } ] 点坐标
 */
function saveLineSidePoint(pointArr) {
    if (!pointArr) {
        throw new Error("请输入合法点集参数！")
    }
    if (pointArr.length === 1) {
        throw new Error("输入的点集至少要有 2 个！")
    }
    if (pointArr.length === 2) {
        return pointArr
    }
    let newArrPoint = pointArr;
    if (pointArr[0].y === pointArr[1].y) {
        // 点集成竖直排列
        newArrPoint = orderArray(pointArr, "x");
        newArrPoint.forEach((point, idx) => {
            if (idx != 0 && idx != newArrPoint.length - 1) {
                point.__SIDE__DYX___ = "center";
            }
        });
        return newArrPoint;
    }
    if (pointArr[0].x === pointArr[1].x) {
        // 点集成水平排列
        newArrPoint = orderArray(pointArr, "y");
        newArrPoint.forEach((point, idx) => {
            if (idx != 0 && idx != newArrPoint.length - 1) {
                point.__SIDE__DYX___ = "center";
            }
        });
        return newArrPoint;
    }
    // 斜着排列也是一样
    newArrPoint = orderArray(pointArr, "x");    // 任意找一个基准进行排列
    // newArrPoint = orderArray(arrPoint, "y"); // 也一样
    newArrPoint.forEach((point, idx) => {
        if (idx != 0 && idx != newArrPoint.length - 1) {
            point.__SIDE__DYX___ = "center";
        }
    });
    return newArrPoint;
}


/**
 * @description: 求向量点乘，内夹角（仅限于用点形式表达的二维向量）
 * @param {*} point1
 * @param {*} point2
 * @return { multiNum, angel } 点积, 两向量所成夹角（角度）
 */
function mutipleProduct(point1, point2) {
    if (!point1 || !point2) {
        return {
            multiNum: 0,
            angel: 0
        }
    };
    let point1Model = Math.sqrt(point1.x * point1.x + point1.y * point1.y)
    let point2Model = Math.sqrt(point2.x * point2.x + point2.y * point2.y)
    return {
        multiNum: point1.x * point2.x + point1.y * point2.y,    // 点积的值
        angel: Math.acos((point1.x * point2.x + point1.y * point2.y) / (point1Model * point2Model)) * 180 / Math.PI,    // 两向量所成夹角（角度）
    };
}

/**
 * @description: 二维向量叉乘的模/三角形面积（以原点为起点）
 * @param {*} point1 点1
 * @param {*} point2 点2
 * @param {*} needTriSquare true 求三角形面积; false 求叉乘的模
 * @return {*}
 */
function cross2Product(point1, point2, needTriSquare = true) {
    // needTriSquare 为 true 是求三角形面积
    // needTriSquare 为 false 是求叉乘的模
    return needTriSquare ? 0.5 * (point1.x * point2.y - point1.y * point2.x) : point1.x * point2.y - point1.y * point2.x
}

/**
 * @description: 计算几何中心 ---> 图形控制点
 * @param {*} pointArr 构成多边形的各个顶点（顺序）
 * @return { x, y } 中心点
 */
function calcPolyCenterPoint(pointArr) {
    if (!pointArr) return null;
    if (pointArr.length === 1) return pointArr[0];
    let xSum = 0
    let ySum = 0
    pointArr.forEach(it => {
        xSum += it.x
        ySum += it.y
    });
    return {
        x: xSum / pointArr.length,
        y: ySum / pointArr.length,
    }
}

/**
 * @description: 求取任意凸、凹多边形面积
 * @param {*} pointArr 多个点
 * @return { realArea, absArea } 真实面积，绝对值面积
 */
function drawPolygon(pointArr) {
    if (!pointArr || pointArr.length < 2) {
        return null;
    }
    /* 求取面积 */
    if (pointArr.length == 2) {
        return {
            realArea: 0, // 真实面积
            absArea: 0,
        }
    }
    let squareArea = 0
    let absArea = 0
    // 转化为相对于起点的各处向量
    let newPointArr = changePointToBase(pointArr[0], pointArr)
    for (let i = 1; i < newPointArr.length; i++) {
        let y = i + 1
        if (y >= newPointArr.length - 1) {
            y = newPointArr.length - 1
        }
        squareArea += cross2Product(newPointArr[i], newPointArr[y])
        absArea += Math.abs(cross2Product(newPointArr[i], newPointArr[y]))
    }
    return {
        realArea: Math.abs(squareArea), // 真实面积
        absArea: absArea,   // 绝对值面积
    };
}

/**
 * @description: 判断多边形是凸、凹多边形
 * @param {*} pointArr 构成多边形的各个顶点（顺序）
 * @return { boolean } true 凸多边形; false 凹多边形
 */
function isConvexOrConcave(pointArr) {
    if (!pointArr) {
        throw new Error("请输入合法参数")
    }
    if (pointArr.length === 1) {
        throw new Error("输入图形是点")
    }
    if (pointArr.length === 2) {
        throw new Error("输入图形是线段")
    }
    let sideSize = pointArr.length; // 输入图形的边数
    let normalAngelSum = sideSize * 180 - 360   // 正常凸多边形的时候的内角和
    let currentAngelSum = 0
    for (let i = 0; i < pointArr.length; i++) {
        let y = i + 1;
        if (i === 0) {
            currentAngelSum = currentAngelSum + mutipleProduct(changePointToBase(pointArr[0], [pointArr[pointArr.length - 1]])[0], changePointToBase(pointArr[0], [pointArr[1]])[0]).angel
        } else {
            currentAngelSum = currentAngelSum + mutipleProduct(changePointToBase(pointArr[i], [pointArr[i - 1]])[0], changePointToBase(pointArr[i], [pointArr[y]])[0]).angel
        }
        if (y >= pointArr.length - 1) {
            currentAngelSum = currentAngelSum + mutipleProduct(changePointToBase(pointArr[y], [pointArr[0]])[0], changePointToBase(pointArr[y], [pointArr[i]])[0]).angel
            break;
        }
    }
    if (Number.isNaN(currentAngelSum)) {
        throw new Error("输入点有重复！")
    }
    return currentAngelSum.toFixed(2) === normalAngelSum.toFixed(2);    // 校准精度
}

/**
 * @description: 获取点1 point1 指向点2 point2 的向量
 * @param {*} point1
 * @param {*} point2
 * @return { x, y } 两者向量差
 */
function getPointArray(point1, point2) {
    return {
        ...point1,
        ...point2,
        x: point2.x - point1.x,
        y: point2.y - point1.y,
    };
}

/**
 * @description: 判断点是否在凸多边形内
 * @param {*} pointArr 构成多边形的各个顶点（顺序）
 * @param {*} point 等待判断的点吧
 * @return { boolean | string } true 在内部; false 在外部; '边' 在边界。
 */
function isInMultiPolygon(pointArr, point) {
    // 首先判断图形是凸的还是凹的
    if (!isConvexOrConcave(pointArr)) {
        throw new Error("当前图形是凹多边形，该函数无法判别！")
        return false;
    }
    let newPointArr = changePointToBase(point, pointArr)
    let basePointAbsArea = 0    // 以基准点为坐标系，与图形的各个顶点相连分割的三角形相加的面积
    // console.log('原多边形的面积 :>> ', drawPolygon(null, pointArr, false));
    for (let i = 0; i < newPointArr.length; i++) {
        let y = i + 1
        if (y === newPointArr.length) {
            y = 0
        }
        if (cross2Product(newPointArr[i], newPointArr[y]) === 0) {
            return '边';
        }
        basePointAbsArea += Math.abs(cross2Product(newPointArr[i], newPointArr[y]))
    }
    return basePointAbsArea.toFixed(3) === drawPolygon(pointArr).realArea.toFixed(3)   // 校准精度
}

/**
 * @description: 求解直线解析式（ Ax + By + C = 0 ）
 * @param {*} pointArr 直线上的任意两个点
 * @return { a, b, c } 返回直线解析式中的系数
 */
function resolveLineParamsFunction(pointArr) {
    if (!pointArr) {
        throw new Error("请输入正确的点的格式！")
    }
    if (pointArr.length < 2) {
        throw new Error("直线由至少两个点确认，请至少输入两个点！")
    }
    // 如果是竖直线
    if (pointArr[1].x - pointArr[0].x === 0) {
        return {
            a: 1,
            b: 0,
            c: -pointArr[0].x,
        };
    }
    // 如果是水平线
    if (pointArr[1].y - pointArr[0].y === 0) {
        return {
            a: 0,
            b: 1,
            c: -pointArr[0].y,
        };
    }
    // 斜率，tan
    const k = (pointArr[1].y - pointArr[0].y) / (pointArr[1].x - pointArr[0].x);
    const b = pointArr[0].y - k * pointArr[0].x;
    return {
        a: k,
        b: -1,
        c: b,
    };
}

/**
 * @description: 求两条直线的交点（方法1 代数方法）
 * @param {*} pointArr1 直线1上的任意两个点
 * @param {*} pointArr2 直线2上的任意两个点
 * @param { number } precision 交点的精度
 * @return { x, y | boolean } 返回交点坐标；当返回布尔值 false 的时候代表两直线重合或者两直线平行
 */
function resolveCrossPoint1(pointArr1, pointArr2, precision = 2) {
    let line1Param = resolveLineParamsFunction(pointArr1);
    let line2Param = resolveLineParamsFunction(pointArr2);
    let d = line1Param.a * line2Param.b - line2Param.a * line1Param.b;
    if (d === 0) {
        if (line1Param.c === line2Param.c) {
            // console.log("直线重合！");
            return false;
        }
        if (line1Param.c != line2Param.c) {
            // console.log("直线平行！");
            return false;
        }
    }

    let x = (-line1Param.c * line2Param.b - (-line2Param.c * line1Param.b)) / d;
    let y = (-line2Param.c * line1Param.a - (-line1Param.c * line2Param.a)) / d;
    return {
        x: x.toFixed(precision) - 0,
        y: y.toFixed(precision) - 0,
    };
}

/**
 * @description: 角度互相转化为弧度
 * @param { number | string } deg 角度/弧度
 * @param { boolean } direction 转化方向，true 角度转化为弧度; false 弧度转化为角度。
 * @return { number } 返回弧度/角度
 */
function degree2Radians(deg, direction = true) {
    if (direction) {
        // 角度转化为弧度
        return (deg * Math.PI) / 180;
    } else {
        // 弧度转化为角度
        return (deg * 180) / Math.PI;
    }
}

/**
 * @description: 经纬度坐标转换为墨卡托坐标系 --- 注意经纬度的本质是角度，不是弧度
 * @param {*} point 经纬度坐标，x：经度；y：纬度
 * @return {*} 返回墨卡托坐标系的坐标
 */
function geoToMercatorCoordinate(point) {
    // 经纬度坐标转化为墨卡托坐标系
    if (!point) {
        throw new Error("输入的点或者点集格式不对！");
    }
    if (point.x > 180 || point.x <= -180) {
        throw new Error("经度范围是 (-180°, 180°]！");
    }
    if (point.y > 90 || point.y <= -90) {
        throw new Error("纬度范围是 [-90°, 90°]！");
    }
    const earthRadius = 6378137; // 地球半径
    if (!Array.isArray(point)) {
        // 单个点
        return {
            ...point,
            x: (earthRadius * Math.PI * point.x) / 180,
            y:
                earthRadius *
                Math.log(Math.tan(Math.PI / 4 + degree2Radians(point.y / 2))),
        };
    } else {
        // 点集
        return point.map((item) => {
            return {
                ...item,
                x: (earthRadius * Math.PI * item.x) / 180,
                y:
                    (earthRadius *
                        Math.PI *
                        Math.log(Math.tan(Math.PI / 4 + item.y / 2))) /
                    180,
            };
        });
    }
}

/**
 * @description: 墨卡托坐标系转换为经纬度坐标 --- 注意经纬度的本质是角度，不是弧度
 * @param {*} point 墨卡托坐标系的坐标，其 x、y 范围都是：[−20037508.34, 20037508.34]
 * @return {*} 返回经纬度，x、y 是角度。
 */
function geoToMercatorCoordinate(point) {
    // 经纬度坐标转化为墨卡托坐标系
    if (!point) {
        throw new Error("输入的点或者点集格式不对！");
    }
    if (point.x > 20037508.34 || point.x < -20037508.34) {
        throw new Error("墨卡托坐标系中 x 坐标范围是 (-20037508.34, 20037508.34]！");
    }
    if (point.y > 20037508.34 || point.y < -20037508.34) {
        throw new Error("墨卡托坐标系中 y 坐标范围是 [-20037508.34, 20037508.34]！");
    }
    const earthRadius = 6378137; // 地球半径
    if (!Array.isArray(point)) {
        // 单个点
        return {
            ...point,
            x: earthRadius * degree2Radians(point.x),
            y: earthRadius * Math.log(Math.tan(Math.PI / 4 + degree2Radians(point.y / 2))),
        };
    } else {
        // 点集
        return point.map((item) => {
            return {
                ...item,
                x: earthRadius * degree2Radians(item.x),
                y: earthRadius * Math.log(Math.tan(Math.PI / 4 + degree2Radians(item.y / 2))),
            };
        });
    }
}
